package com.assembler.app.debruij.succinct;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.assembler.app.debruij.model.CompositeItem;
import com.assembler.app.debruij.model.Edge;
import com.assembler.app.debruij.model.Node;

/**
 * This class provides the Succinct Representation for De Bruijn Graphto
 * represent a network of overlapping short read data.
 * 
 * This class is a part of the lab rotation project for PhD - students January
 * 2014. School of Computing National University Singapore.
 * 
 * @author Iana Pyrogova, Abha Belorkar
 * @version 1.0.1
 */
public class DeBruijnGraph extends AbstractGraph {

	protected static final char STARTING_SYMBOL = '$';

	private static final int ALPHABET_SIZE = 4;

	private List<CompositeItem> composites;

	private final int kMerLength;

	private final String sequence;

	private Map<String, Integer> fMap = new HashMap<>(ALPHABET_SIZE + 1);

	protected DeBruijnGraph(String sequence, int kMerLength) {
		this.sequence = normalizeSequence(sequence, kMerLength);
		this.kMerLength = kMerLength;
	}

	protected String normalizeSequence(String sequence, int kMerLength) {
		String sufix = new String(new char[kMerLength]).replace('\0', STARTING_SYMBOL);
		return sufix + sequence + STARTING_SYMBOL;
	}

	/**
	 * Returns the position of the ith occurrence of edge (value) in the list of
	 * edges.
	 */
	public int selectEdge(Edge value, int ith) {
		int currentPos = 1;
		for (int j = 0; j < composites.size(); j++) {
			if (composites.get(j).getEdge().equals(value)) {
				if (currentPos == ith) {
					return j;
				} else {
					currentPos++;
				}
			}
		}
		return -1;
	}

	/**
	 * Returns the position of the ith (occuranceCount) occurence of value in
	 * the binar array L.
	 */
	public int selectL(boolean value, int occuranceCount) {
		int currentPos = 1;
		for (int j = 0; j < composites.size(); j++) {
			if (composites.get(j).isL().equals(value)) {
				if (currentPos == occuranceCount) {
					return j;
				} else {
					currentPos++;
				}
			}
		}
		return -1;
	}

	/**
	 * Return index of the last edge of the node pointed to by edge with
	 * edgeIndex
	 * 
	 * @param edgeIndex
	 * @return Return index of the last edge of the node
	 */
	public int forward(int edgeIndex) {
		List<Boolean> l = getLList();
		List<Edge> edges = getEdgesList();
		Edge targetEdge = composites.get(edgeIndex).getEdge();
		Integer indexInMap = getfMap().get(targetEdge.getEdge());
		int rankC = rank(edgeIndex, edges, targetEdge);
		int rankToBase = rank(indexInMap, l, true);
		return selectL(true, rankC + rankToBase);
	}

	/**
	 * Return index of the first edge that points to the node that the edge at
	 * nodeIndex exits.
	 * 
	 * @param nodeIndex
	 * @return Return index of the first edge that points to the node
	 */
	public int backward(int nodeIndex) {
		String nodeValue = composites.get(nodeIndex).getNode().getNode();
		Edge edgeValue = new Edge(Character.toString(nodeValue.charAt(nodeValue.length() - 1)));

		Integer indexInMap = getfMap().get(edgeValue.getEdge());
		List<Boolean> l = getLList();

		int rank1 = rank(indexInMap, l, true);
		int rank2 = rank(nodeIndex, l, true);
		int indexW = rank2 - rank1;
		return selectEdge(edgeValue, indexW);
	}

	/**
	 * Return number of incoming edges to node with index nodeIndex
	 * 
	 * @param nodeIndex
	 * @return Return number of incoming edges to node
	 */
	public int indegree(int nodeIndex) {
		int indexEdgeBkw = backward(nodeIndex);
		int rankEdge = rank(nodeIndex, getEdgesList(), getEdgesList().get(indexEdgeBkw));
		int indexForLastOccOfEdge = selectEdge(getEdgesList().get(indexEdgeBkw), rankEdge);
		Edge edgeWithLabel = getEdgesList().get(indexEdgeBkw);
		edgeWithLabel.setLabel(false);
		int rankMinusEdge1 = rank(indexEdgeBkw, getEdgesList(), edgeWithLabel);
		int rankMinusEdge2 = rank(indexForLastOccOfEdge, getEdgesList(), edgeWithLabel);
		return rankMinusEdge2 - rankMinusEdge1 + 1;
	}

	/**
	 * Returns the target node index after traversing edge 'edge' from node with
	 * index nodeIndex
	 * 
	 * @param nodeIndex
	 * @param edge
	 * @return Returns the target node index
	 */
	public int outgoing(int nodeIndex, Edge edge) {
		int rank1 = rank(nodeIndex, getEdgesList(), edge);
		int selectIndex = selectEdge(edge, rank1);
		return forward(selectIndex);
	}

	/**
	 * Returns the index of the predecessor node starting with symbol
	 * startSymbol, that has an edge to node nodeIndex.
	 * 
	 * @param nodeIndex
	 * @param startSymbol
	 * @return
	 */
	public int incoming(int nodeIndex, String startSymbol) {
		int indexEdgeBkw = backward(nodeIndex);
		int indegreeV = indegree(nodeIndex);
		for (int j = indexEdgeBkw; j <= indexEdgeBkw + indegreeV; j++) {
			CompositeItem item = composites.get(j);
			if (item.getNode().getNode().startsWith(startSymbol)) {
				return j;
			}
		}
		return -1;
	}

	/**
	 * Return number of outgoing edges from node
	 */
	public Integer outdegree(Node node, List<Node> nodesList) {
		Integer count = 0;
		count = nodesList.lastIndexOf(node) - nodesList.indexOf(node) + 1;
		return count;
	}
	/**
	 * Return all parents for node
	 */
	public List<CompositeItem> getAllChildrenForNode(int nodeIndex){
		List<CompositeItem> children = new ArrayList<CompositeItem>();
		if(outgoing(nodeIndex, new Edge("A"))!= -1){
			children.add(composites.get(outgoing(nodeIndex, new Edge("A"))));
		}
		if(outgoing(nodeIndex, new Edge("C"))!= -1){
			children.add(composites.get(outgoing(nodeIndex, new Edge("C"))));
		}
		if(outgoing(nodeIndex, new Edge("G"))!= -1){
			children.add(composites.get(outgoing(nodeIndex, new Edge("G"))));
		}
		if(outgoing(nodeIndex, new Edge("T"))!= -1){
			children.add(composites.get(outgoing(nodeIndex, new Edge("T"))));
		}
		return children;
	}
	
	/**
	 * return all parents for node.
	 * @param nodeIndex
	 * @param edge
	 * @return
	 */
	public List<CompositeItem> getAllParentsForNode(int nodeIndex){
		List<CompositeItem> parents = new ArrayList<CompositeItem>();
		if(incoming(nodeIndex, "A")!= -1){
			parents.add(composites.get(incoming(nodeIndex, "A")));
		}
		if(incoming(nodeIndex, "C")!= -1){
			parents.add(composites.get(incoming(nodeIndex, "C")));
		}
		if(incoming(nodeIndex, "G")!= -1){
			parents.add(composites.get(incoming(nodeIndex, "G")));
		}
		if(incoming(nodeIndex, "T")!= -1){
			parents.add(composites.get(incoming(nodeIndex, "T")));
		}
		
		return parents;
	}
	private int childIndex(int nodeIndex, String edge){
		return outgoing(nodeIndex, new Edge(edge));
	}
	private int parentIndex(int nodeIndex, String startSymbol){
		return incoming(nodeIndex, startSymbol);
	}
	// -- getters and setters --
	public List<CompositeItem> getComposites() {
		return composites;
	}

	public void setComposites(List<CompositeItem> composites) {
		this.composites = composites;
	}

	public Map<String, Integer> getfMap() {
		return fMap;
	}

	public void setfMap(Map<String, Integer> fMap) {
		this.fMap = fMap;
	}

	public int getkMerLength() {
		return kMerLength;
	}

	public String getSequence() {
		return sequence;
	}

	protected List<Boolean> getLList() {
		List<Boolean> res = new ArrayList<Boolean>();
		for (int j = 0; j < composites.size(); j++) {
			res.add(composites.get(j).isL());
		}
		return res;
	}

	public List<Edge> getEdgesList() {
		List<Edge> res = new ArrayList<Edge>();
		for (int j = 0; j < composites.size(); j++) {
			res.add(composites.get(j).getEdge());
		}
		return res;
	}

	protected List<Node> getNodeList() {
		List<Node> res = new ArrayList<Node>();
		for (int j = 0; j < composites.size(); j++) {
			res.add(composites.get(j).getNode());
		}
		return res;
	}

	protected List<Node> getNodeListByPrefix(String prefix) {
		List<Node> res = new ArrayList<Node>();
		for (int j = 0; j < composites.size(); j++) {
			if (composites.get(j).getNode().getNode().startsWith(prefix)) {
				res.add(composites.get(j).getNode());
			}
		}
		return res;
	}

	public List<Node> getUnicNodeList() {
		Set<Node> uniqSet = new HashSet<>(getNodeList());
		return new ArrayList<Node>(uniqSet);
	}
}
